| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756 |
- <?xml version="1.0" encoding="UTF-8"?>
- <!-- Reviewed: no -->
- <sect1 id="learning.quickstart.create-model">
- <title>Create a Model and Database Table</title>
- <para>
- Before we get started, let's consider something: where will these classes live, and how will
- we find them? The default project we created instantiates an autoloader. We can attach other
- autoloaders to it so that it knows where to find different classes. Typically, we want our
- various MVC classes grouped under the same tree -- in this case,
- <filename>application/</filename> -- and most often using a common prefix.
- </para>
- <para>
- <classname>Zend_Controller_Front</classname> has a notion of "modules", which are individual
- mini-applications. Modules mimic the directory structure that the <command>zf</command>
- tool sets up under <filename>application/</filename>, and all classes inside them are
- assumed to begin with a common prefix, the module name. <filename>application/</filename>
- is itself a module -- the "default" or "application" module. As such, we'll want to setup
- autoloading for resources within this directory.
- </para>
- <para>
- <classname>Zend_Application_Module_Autoloader</classname> provides the functionality needed
- to map the various resources under a module to the appropriate directories, and provides a
- standard naming mechanism as well. An instance of the class is created by default during
- initialization of the bootstrap object; your application bootstrap will by default use the
- module prefix "Application". As such, our models, forms, and table classes will all begin
- with the class prefix "Application_".
- </para>
- <para>
- Now, let's consider what makes up a guestbook. Typically, they are simply a list of entries
- with a <emphasis>comment</emphasis>, <emphasis>timestamp</emphasis>, and, often,
- <emphasis>email address</emphasis>. Assuming we store them in a database, we may also want a
- <emphasis>unique identifier</emphasis> for each entry. We'll likely want to be able to save
- an entry, fetch individual entries, and retrieve all entries. As such, a simple guestbook
- model <acronym>API</acronym> might look something like this:
- </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> and <methodname>__set()</methodname> will provide a
- convenience mechanism for us to access the individual entry properties, and proxy to the
- other getters and setters. They also will help ensure that only properties we whitelist will
- be available in the object.
- </para>
- <para>
- <methodname>find()</methodname> and <methodname>fetchAll()</methodname> provide the ability
- to fetch a single entry or all entries, while <methodname>save()</methodname> takes care of
- saving an entry to the data store.
- </para>
- <para>
- Now from here, we can start thinking about setting up our database.
- </para>
- <para>
- First we need to initialize our <classname>Db</classname> resource. As with the
- <classname>Layout</classname> and <classname>View</classname> resource, we can provide
- configuration for the <classname>Db</classname> resource. We can do this with the
- <command>zf configure db-adapter</command> 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>
- Now edit your <filename>application/configs/application.ini</filename> file, where you'll
- see the following lines were added in the appropriate sections.
- </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>
- Your final configuration file should look like the following:
- </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>
- Note that the database(s) will be stored in <filename>data/db/</filename>. Create those
- directories, and make them world-writeable. On unix-like systems, you can do that as
- follows:
- </para>
- <programlisting language="shell"><![CDATA[
- % mkdir -p data/db; chmod -R a+rwX data
- ]]></programlisting>
- <para>
- On Windows, you will need to create the directories in Explorer and set the permissions to
- allow anyone to write to the directory.
- </para>
- <para>
- At this point we have a connection to a database; in our case, its a connection to a Sqlite
- database located inside our <filename>application/data/</filename> directory. So, let's
- design a simple table that will hold our guestbook entries.
- </para>
- <programlisting language="sql"><![CDATA[
- -- 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 'noemail@test.com',
- comment TEXT NULL,
- created DATETIME NOT NULL
- );
- CREATE INDEX "id" ON "guestbook" ("id");
- ]]></programlisting>
- <para>
- And, so that we can have some working data out of the box, lets create a few rows of
- information to make our application interesting.
- </para>
- <programlisting language="sql"><![CDATA[
- -- scripts/data.sqlite.sql
- --
- -- You can begin populating the database with the following SQL statements.
- 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>
- Now that we have both the schema and some data defined. Lets get a script together that we
- can now execute to build this database. Naturally, this is not needed in production, but
- this script will help developers build out the database requirements locally so they can
- have the fully working application. Create the script as
- <filename>scripts/load.sqlite.php</filename> with the following contents:
- </para>
- <programlisting language="php"><![CDATA[
- // scripts/load.sqlite.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;
- ]]></programlisting>
- <para>
- Now, let's execute this script. From a terminal or the DOS command line, do the following:
- </para>
- <programlisting language="shell"><![CDATA[
- % php scripts/load.sqlite.php --withdata
- ]]></programlisting>
- <para>
- You should see output like the following:
- </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>
- Now we have a fully working database and table for our guestbook application. Our next few
- steps are to build out our application code. This includes building a data source (in our
- case, we will use <classname>Zend_Db_Table</classname>), and a data mapper to connect that
- data source to our domain model. Finally we'll also create the controller that will interact
- with this model to both display existing entries and process new entries.
- </para>
- <para>
- We'll use a <ulink url="http://martinfowler.com/eaaCatalog/tableDataGateway.html">Table Data
- Gateway</ulink> to connect to our data source; <classname>Zend_Db_Table</classname>
- provides this functionality. To get started, lets create a
- <classname>Zend_Db_Table</classname>-based table class. Just as we've done for layouts and
- the database adapter, we can use the <command>zf</command> tool to assist, using the command
- <command>create db-table</command>. This takes minimally two arguments, the name by which
- you want to refer to the class, and the database table it maps to.
- </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>
- Looking at your directory tree, you'll now see that a new directory,
- <filename>application/models/DbTable/</filename>, was created, with the file
- <filename>Guestbook.php</filename>. If you open that file, you'll see the following
- contents:
- </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>
- Note the class prefix: <classname>Application_Model_DbTable</classname>. The class prefix
- for our module, "Application", is the first segment, and then we have the component,
- "Model_DbTable"; the latter is mapped to the <filename>models/DbTable/</filename> directory
- of the module.
- </para>
- <para>
- All that is truly necessary when extending <classname>Zend_Db_Table</classname> is to
- provide a table name and optionally the primary key (if it is not "id").
- </para>
- <para>
- Now let's create a <ulink url="http://martinfowler.com/eaaCatalog/dataMapper.html">Data
- Mapper</ulink>. A <emphasis>Data Mapper</emphasis> maps a domain object to the database.
- In our case, it will map our model, <classname>Application_Model_Guestbook</classname>, to
- our data source, <classname>Application_Model_DbTable_Guestbook</classname>. A typical
- <acronym>API</acronym> for a data mapper is as follows:
- </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>
- In addition to these methods, we'll add methods for setting and retrieving the Table Data
- Gateway. To create the initial class, use the <command>zf</command> CLI tool:
- </para>
- <programlisting language="shell"><![CDATA[
- % zf create model GuestbookMapper
- Creating a model at application/models/GuestbookMapper.php
- Updating project profile '.zfproject.xml'
- ]]></programlisting>
- <para>
- Now, edit the class <classname>Application_Model_GuestbookMapper</classname> found in
- <filename>application/models/GuestbookMapper.php</filename> to read as follows:
- </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>
- Now it's time to create our model class. We'll do so, once again, using the
- <command>zf create model</command> 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>
- We'll modify this empty <acronym>PHP</acronym> class to make it easy to populate the model
- by passing an array of data either to the constructor or a
- <methodname>setOptions()</methodname> method. The final model class, located in
- <filename>application/models/Guestbook.php</filename>, should look like this:
- </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>
- Lastly, to connect these elements all together, lets create a guestbook controller that will
- both list the entries that are currently inside the database.
- </para>
- <para>
- To create a new controller, use the <command>zf create controller</command> 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>
- This will create a new controller, <classname>GuestbookController</classname>, in
- <filename>application/controllers/GuestbookController.php</filename>, with a single action
- method, <methodname>indexAction()</methodname>. It will also create a view script directory
- for the controller, <filename>application/views/scripts/guestbook/</filename>, with a view
- script for the index action.
- </para>
- <para>
- We'll use the "index" action as a landing page to view all guestbook entries.
- </para>
- <para>
- Now, let's flesh out the basic application logic. On a hit to
- <methodname>indexAction()</methodname>, we'll display all guestbook entries. This would look
- like the following:
- </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>
- And, of course, we need a view script to go along with that. Edit
- <filename>application/views/scripts/guestbook/index.phtml</filename> to read as follows:
- </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>Checkpoint</title>
- <para>
- Now browse to "http://localhost/guestbook". You should see the following in your
- browser:
- </para>
- <para>
- <inlinegraphic width="525" scale="100" align="center" valign="middle"
- fileref="figures/learning.quickstart.create-model.png" format="PNG" />
- </para>
- </note>
- <note>
- <title>Using the data loader script</title>
- <para>
- The data loader script introduced in this section
- (<filename>scripts/load.sqlite.php</filename>) can be used to create the database for
- each environment you have defined, as well as to load it with sample data. Internally,
- it utilizes <classname>Zend_Console_Getopt</classname>, which allows it to provide a
- number of command line switches. If you pass the "-h" or "--help" switch, it will give
- you the available options:
- </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>
- The "-e" switch allows you to specify the value to use for the constant
- <constant>APPLICATION_ENV</constant> -- which in turn allows you to create a SQLite
- database for each environment you define. Be sure to run the script for the environment
- you choose for your application when deploying.
- </para>
- </note>
- </sect1>
|