Web Frameworks - Workbook - Week 04

From mi-linux
Revision as of 13:45, 19 January 2010 by In9352 (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

Main Page >> Web Frameworks >> Workbook >> Workshop - week 04

Create a Model and Database Table

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, application/ -- and most often using a common prefix.

Zend_Controller_Front has a notion of "modules", which are individual mini-applications. Modules mimic the directory structure that the zf tool sets up under application/, and all classes inside them are assumed to begin with a common prefix, the module name. application/ is itself a module -- the "default" module. As such, let's setup autoloading for resources within this directory, giving them a prefix of "Default". We can do this by creating another bootstrap resource.

Step 1 - Set-up autoload

Zend_Application_Module_Autoloader provides the functionality needed to map the various resources under a module to the appropriate directories, and provides a standard naming mechanism as well. In our bootstrap resource, we'll instantiate this, and be done. The method looks like this:

// application/Bootstrap.php

// Add this method to the Bootstrap class:

    protected function _initAutoload()
    {
        $autoloader = new Zend_Application_Module_Autoloader(array(
            'namespace' => 'Default_',
            'basePath'  => dirname(__FILE__),
        ));
        return $autoloader;
    }

The final bootstrap class will look as follows:

<?php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initAutoload()
    {
        $autoloader = new Zend_Application_Module_Autoloader(array(
            'namespace' => 'Default',
            'basePath'  => dirname(__FILE__),
        ));
        return $autoloader;
    }

    protected function _initDoctype()
    {
        $this->bootstrap('view');
        $view = $this->getResource('view');
        $view->doctype('XHTML1_STRICT');
    }
}

Step 2 - Create your model class

Now, let's consider what makes up a guestbook. Typically, they are simply a list of entries with a comment, timestamp, and, often, email address. Assuming we store them in a database, we may also want a unique identifier 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 API might look something like this:

// application/models/Guestbook.php

class Default_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();

    public function save();
    public function find($id);
    public function fetchAll();
}

__get() and __set() 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.

find() and fetchAll() provide the ability to fetch a single entry or all entries.

Now from here, we can start thinking about setting up our database.

Step 3 - Create database table

Let's create this simple table:

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

... and populate it with some test data:

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

You can do this via "phpmyadmin": https://mi-linux.wlv.ac.uk/phpmyadmin/

If you haven't registered for a Mysql database yet, you can do so here: https://mi-linux.wlv.ac.uk/facilities/

Once you have created your Mysql database, simply add its connection details to your app.ini file:

Step 4 - initialize our Db resource

Then we need to initialize our Db resource. As with the Layout and View resource, we can provide configuration for the Db resource. In your application/configs/application.ini file, add the following lines in the appropriate sections:

[production]
resources.db.adapter       = "PDO_MYSQL"
resources.db.params.host   = "localhost"
resources.db.params.username   = "YOUR STUDENT NUMBER"
resources.db.params.password   = "YOUR DB PASSWORD"
resources.db.params.dbname = "dbYOUR STUDENT NUMBER"

[development : production]
resources.db.params.host   = "localhost"
resources.db.params.username   = "YOUR STUDENT NUMBER"
resources.db.params.password   = "YOUR DB PASSWORD"
resources.db.params.dbname = "dbYOUR STUDENT NUMBER"

[testing : production]
resources.db.params.host   = "localhost"
resources.db.params.username   = "YOUR STUDENT NUMBER"
resources.db.params.password   = "YOUR DB PASSWORD"
resources.db.params.dbname = "dbYOUR STUDENT NUMBER"

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 Zend_Db_Table), 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.

Step 5 - connect to our data source

We'll use a Table Data Gateway to connect to our data source; Zend_Db_Table provides this functionality. To get started, lets create a Zend_Db_Table-based table class. First, create the directory application/models/DbTable/. Then create and edit a file Guestbook.php within it, and add the following contents:

<?php
// application/models/DbTable/Guestbook.php

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

Note the class prefix: Default_Model_DbTable. The class prefix "Default" from our autoloader is the first segment, and then we have the component, "Model_DbTable"; the latter is mapped to the models/DbTable/ directory of the module.

All that is truly necessary when extending Zend_Db_Table is to provide a table name and optionally the primary key (if it is not "id").

Step 6 - create a Data Mapper

Now let's create a Data Mapper. A Data Mapper maps a domain object to the database. In our case, it will map our model, Default_Model_Guestbook, to our data source, Default_Model_DbTable_Guestbook. A typical API for a data mapper is as follows:

// application/models/GuestbookMapper.php

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

In addition to these methods, we'll add methods for setting and retrieving the Table Data Gateway. The final class, located in application/models/GuestbookMapper.php, looks like this:

<?php
// application/models/GuestbookMapper.php

class Default_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('Default_Model_DbTable_Guestbook');
        }
        return $this->_dbTable;
    }

    public function save(Default_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, Default_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 Default_Model_Guestbook();
            $entry->setId($row->id)
                  ->setEmail($row->email)
                  ->setComment($row->comment)
                  ->setCreated($row->created)
                  ->setMapper($this);
            $entries[] = $entry;
        }
        return $entries;
    }
}

Step 7 - Update model

Now it's time to update our model class slightly, to accomodate the data mapper. Just like the data mapper contains a reference to the data source, the model contains a reference to the data mapper. Additionally, we'll make it easy to populate the model by passing an array of data either to the constructor or a setOptions() method. The final model class, located in application/models/Guestbook.php, looks like this:

<?php
// application/models/Guestbook.php

class Default_Model_Guestbook
{
    protected $_comment;
    protected $_created;
    protected $_email;
    protected $_id;
    protected $_mapper;

    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;
    }

    public function setMapper($mapper)
    {
        $this->_mapper = $mapper;
        return $this;
    }

    public function getMapper()
    {
        if (null === $this->_mapper) {
            $this->setMapper(new Default_Model_GuestbookMapper());
        }
        return $this->_mapper;
    }

    public function save()
    {
        $this->getMapper()->save($this);
    }

    public function find($id)
    {
        $this->getMapper()->find($id, $this);
        return $this;
    }

    public function fetchAll()
    {
        return $this->getMapper()->fetchAll();
    }
}

Step 8 - Create a controller and a view

Lastly, to connect these elements all together, lets create a guestbook controller that will both list the entries that are currently inside the database.

To create a new controller, open command line console, navigate to your project directory, and enter the following:

% zf.sh create controller guestbook

If all goes well you should see the following messages:

  • Creating a controller at /home/staff/acad/in9352/public_html/zend/quickstart/application/controllers/GuestbookController.php
  • Creating an index action method in controller guestbook
  • Creating a view script for the index action method at /home/staff/acad/in9352/public_html/zend/quickstart/application/views/scripts/guestbook/index.phtml
  • Creating a controller test file at /home/staff/acad/in9352/public_html/zend/quickstart/tests/application/controllers/GuestbookControllerTest.php
  • Updating project profile '/home/staff/acad/in9352/public_html/zend/quickstart/.zfproject.xml'

This will create a new controller, GuestbookController, in application/controllers/GuestbookController.php, with a single action method, indexAction(). It will also create a view script directory for the controller, application/views/scripts/guestbook/, with a view script for the index action.

Important: Remember that every time a file is created on mi-linux, you need to make it accessible using the chmod command (see previous sections).

Step 9 - Edit the controller

We'll use the "index" action as a landing page to view all guestbook entries.

Now, let's flesh out the basic application logic. On a hit to indexAction, we'll display all guestbook entries. This would look like the following:

<?php
// application/controllers/GuestbookController.php

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

Step 10 - Edit the view

And, of course, we need a view script to go along with that. Edit application/views/scripts/guestbook/index.phtml to read as follows:

<!-- 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>

Bg-checkpoint.png Checkpoint

Browse to your project: http://mi-linux.wlv.ac.uk/~0123456/quickstart/public/guestbook

Note: We are requesting the "guestbook" controller this time.

Zend 03.jpg

You should see the index of your guestbook... well done!

Note that the "Sign Our Guestbook" link does not work yet, as we haven't implemented a controller/action for it yet!